package cn.iocoder.yudao.module.pay.transfer.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.business.basic.dao.repository.SerialNoRedisRepository;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.exception.Assertions;
import cn.iocoder.yudao.framework.common.exception.KnownServiceException;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.api.infrastructure.config.ConfigApiV2;
import cn.iocoder.yudao.module.api.infrastructure.config.dto.PayConfigRespDTO;
import cn.iocoder.yudao.module.api.infrastructure.social.SocialUserApi;
import cn.iocoder.yudao.module.api.infrastructure.social.dto.SocialUserRespDTO;
import cn.iocoder.yudao.module.api.infrastructure.social.enums.SocialTypeEnum;
import cn.iocoder.yudao.module.api.infrastructure.wechat.WechatClientApi;
import cn.iocoder.yudao.module.api.pay.transfer.dto.PayTransferCreateDTO;
import cn.iocoder.yudao.module.api.pay.transfer.dto.UnifiedTransferCreateDTO;
import cn.iocoder.yudao.module.api.pay.transfer.enums.UnifiedTransferChannelEnum;
import cn.iocoder.yudao.module.api.pay.transfer.enums.UnifiedTransferStatusEnum;
import cn.iocoder.yudao.module.api.pay.transfer.vo.UnifiedTransferVO;
import cn.iocoder.yudao.module.pay.framework.PayClient;
import cn.iocoder.yudao.module.pay.framework.impl.cash.CashPayClient;
import cn.iocoder.yudao.module.pay.framework.impl.mallbook.MallBookWxLitePayClient;
import cn.iocoder.yudao.module.pay.framework.impl.wallet.WalletPayClient;
import cn.iocoder.yudao.module.pay.framework.impl.weixin.WxLitePayClient;
import cn.iocoder.yudao.module.pay.transfer.convert.PayTransferConvert;
import cn.iocoder.yudao.module.pay.transfer.dal.dataobject.PayTransferDO;
import cn.iocoder.yudao.module.pay.transfer.dal.mysql.PayTransferMapper;
import cn.iocoder.yudao.module.pay.transfer.mq.TransferProducer;
import cn.iocoder.yudao.module.pay.transfer.vo.PayTransferPageReqVO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getUserAgent;
import static cn.iocoder.yudao.module.api.infrastructure.ConfigConstants.CONFIG_PAY;
import static cn.iocoder.yudao.module.api.pay.ConfigConstants.PAY_TRANSFER_NO_PREFIX;
import static cn.iocoder.yudao.module.api.pay.ErrorCodeConstants.PAY_TRANSFER_NOT_FOUND;

/**
 * 转账 Service 实现类
 *
 * @author jason
 * @author 山野羡民
 */
@Service
@Slf4j
public class PayTransferServiceImpl implements PayTransferService {

    @Resource
    private PayTransferMapper payTransferMapper;

    @Resource
    private ConfigApiV2 configApiV2;
    @Resource
    private WechatClientApi wechatClientApi;
    @Resource
    private SocialUserApi socialUserApi;

    @Resource
    private TransferProducer transferProducer;
    @Resource
    private SerialNoRedisRepository serialNoRedisRepository;

    private final Lock lock = new ReentrantLock();

    @Override
    public String createTransfer(PayTransferCreateDTO dto) {
        String notifyUrl = configApiV2.getConfig(CONFIG_PAY, PayConfigRespDTO.class).getTransferNotifyUrl();
        if (StrUtil.isNotBlank(dto.getChannelCode())) {
            notifyUrl += "/" + dto.getChannelCode();
        }
        // 创建转账单
        PayTransferDO transfer = PayTransferConvert.INSTANCE.convert(dto);
        transfer.setUserIp(getClientIP());
        transfer.setUserUa(getUserAgent());
        transfer.setNo(serialNoRedisRepository.generateNo(PAY_TRANSFER_NO_PREFIX));
        transfer.setStatus(UnifiedTransferStatusEnum.WAITING.getValue());
        transfer.setNotifyUrl(notifyUrl);
        payTransferMapper.insert(transfer);
        // 发起第三方渠道转账
        doUnifiedTransferAsync(dto, transfer);
        return transfer.getNo();
    }

    @Async
    public void doUnifiedTransferAsync(PayTransferCreateDTO dto, PayTransferDO transfer) {
        PayTransferDO updateObj = new PayTransferDO();
        updateObj.setId(transfer.getId());
        updateObj.setChannelExtras(dto.getChannelExtras());
        updateObj.setStatus(UnifiedTransferStatusEnum.WAITING.getValue());
        try {
            // 调用三方渠道发起转账
            UnifiedTransferCreateDTO unifiedTransferCreateDTO = buildUnifiedTransferCreateDTO(transfer, dto);
            UnifiedTransferVO transferVO = getPayClient(dto).unifiedTransfer(unifiedTransferCreateDTO);
            updateObj.setChannelTransferNo(transferVO.getChannelTransferNo());
            updateObj.setChannelPackageInfo(transferVO.getChannelPackageInfo());
            updateObj.setChannelNotifyData(JsonUtils.toJsonString(transferVO));
            updateObj.setChannelErrorCode(transferVO.getChannelErrorCode());
            updateObj.setChannelErrorMsg(transferVO.getChannelErrorMsg());
        } catch (Throwable e) {
            // 注意这里仅打印异常，不进行抛出。
            // 原因是：虽然调用支付渠道进行转账发生异常（网络请求超时），实际转账成功。这个结果，后续转账轮询可以拿到。
            // 或者使用相同 no 再次发起转账请求
            log.error("[doUnifiedTransferAsync][转账 id({}) dto({}) 发生异常]", transfer.getId(), dto, e);
            updateObj.setChannelNotifyData(ExceptionUtil.stacktraceToOneLineString(e));
            updateObj.setChannelErrorCode("-1");
            updateObj.setChannelErrorMsg(e.getMessage());
        }
        payTransferMapper.updateById(updateObj);
    }

    private UnifiedTransferCreateDTO buildUnifiedTransferCreateDTO(PayTransferDO transfer, PayTransferCreateDTO dto) {
        UnifiedTransferCreateDTO ret = new UnifiedTransferCreateDTO();
        ret.setTransferNo(transfer.getTransferNo());
        ret.setUserName(transfer.getUserName());
        ret.setUserAccount(transfer.getUserAccount());
        ret.setPrice(transfer.getPrice());
        ret.setSubject(transfer.getSubject());
        ret.setNotifyUrl(transfer.getNotifyUrl());
        ret.setChannelExtras(dto.getChannelExtras());
        return ret;
    }

    private PayClient<?> getPayClient(PayTransferCreateDTO dto) {
        PayClient<?> payClient;
        switch (UnifiedTransferChannelEnum.codeOf(dto.getChannelCode())) {
            case OFFLINE: {
                payClient = SpringUtil.getBean(CashPayClient.class);
                break;
            }
            case WALLET: {
                payClient = SpringUtil.getBean(WalletPayClient.class);
                break;
            }
            case WX_BALANCE: {
                String openid = dto.getUserAccount();
                if (StrUtil.isBlank(openid)) {
                    Long userId = null;
                    try {
                        userId = MapUtil.getLong(dto.getChannelExtras(), "user_id", null);
                    } catch (Exception ignore) {
                    }
                    openid = MapUtil.getStr(dto.getChannelExtras(), "openid", "");
                    if (StrUtil.isBlank(openid) && userId != null && userId > 0) {
                        SocialUserRespDTO socialUserRespDTO = socialUserApi.getSocialUserByUserId(UserTypeEnum.MEMBER.getValue(), userId, SocialTypeEnum.WECHAT_MINI_PROGRAM.getType());
                        if (socialUserRespDTO != null) {
                            openid = socialUserRespDTO.getOpenid();
                        }
                    }
                }
                if (StrUtil.isBlank(openid)) {
                    // 未登录的情况下支付，如扫商家收款码付款，此时需要通过`wx.login`静默获取 code 来换取 openid
                    String authCode = MapUtil.getStr(dto.getChannelExtras(), "auth_code", "");
                    if (StrUtil.isBlank(authCode)) {
                        authCode = MapUtil.getStr(dto.getChannelExtras(), "code", "");
                    }
                    if (StrUtil.isNotBlank(authCode)) {
                        openid = wechatClientApi.getWxMaOpenId(authCode);
                    }
                }
                Assertions.isTrue(StrUtil.isNotBlank(openid), "微信开放标识获取失败，无法提现到微信余额");
                dto.getChannelExtras().put("openid", openid);
                payClient = SpringUtil.getBean(WxLitePayClient.class);
                break;
            }
            case MALLBOOK: {
                String channelUserId = dto.getUserAccount();
                Assertions.isTrue(StrUtil.isNotBlank(channelUserId), "渠道商户号获取失败，无法提现到银行卡");
                payClient = SpringUtil.getBean(MallBookWxLitePayClient.class);
                break;
            }
            default:
                throw new KnownServiceException("转账渠道无效：" + dto.getChannelCode());
        }
        return payClient;
    }

    @Override
    public PayTransferDO getTransfer(Long id) {
        return payTransferMapper.selectById(id);
    }

    @Override
    public PayTransferDO getTransferByNo(String no) {
        return payTransferMapper.selectByNo(no);
    }

    @Override
    public PageResult<PayTransferDO> getTransferPage(PayTransferPageReqVO pageReqVO) {
        return payTransferMapper.selectPage(pageReqVO);
    }

    @Override
    public int syncTransfer() {
        List<PayTransferDO> list = payTransferMapper.selectListByStatus(UnifiedTransferStatusEnum.WAITING.getValue());
        if (CollUtil.isEmpty(list)) {
            return 0;
        }
        int count = 0;
        for (PayTransferDO transfer : list) {
            count += syncTransfer(transfer) ? 1 : 0;
        }
        return count;
    }

    @Override
    public void syncTransfer(Long id) {
        PayTransferDO transfer = payTransferMapper.selectById(id);
        if (transfer == null) {
            throw exception(PAY_TRANSFER_NOT_FOUND);
        }
        syncTransfer(transfer);
    }

    @Override
    public boolean syncTransfer(PayTransferDO transfer) {
        try {
            lock.lock();
            PayClient<?> payClient;
            if (Objects.equals(UnifiedTransferChannelEnum.OFFLINE.getCode(), transfer.getChannelCode())) {
                payClient = SpringUtil.getBean(CashPayClient.class);
            } else if (Objects.equals(UnifiedTransferChannelEnum.WALLET.getCode(), transfer.getChannelCode())) {
                payClient = SpringUtil.getBean(WalletPayClient.class);
            } else if (Objects.equals(UnifiedTransferChannelEnum.WX_BALANCE.getCode(), transfer.getChannelCode())) {
                payClient = SpringUtil.getBean(WxLitePayClient.class);
            } else if (Objects.equals(UnifiedTransferChannelEnum.MALLBOOK.getCode(), transfer.getChannelCode())) {
                payClient = SpringUtil.getBean(MallBookWxLitePayClient.class);
            } else {
                log.warn("[syncTransfer] 不支持的渠道({})", transfer.getChannelCode());
                return false;
            }
            UnifiedTransferVO transferVO = payClient.getTransfer(transfer.getTransferNo());
            String rawData = transferVO.getRawData() == null ? "" : JsonUtils.toJsonString(transferVO.getRawData());
            LambdaUpdateWrapper<PayTransferDO> updateWrapper = new LambdaUpdateWrapper<PayTransferDO>()
                    .eq(PayTransferDO::getNo, transfer.getNo());
            updateWrapper.set(PayTransferDO::getChannelNotifyData, rawData);
            updateWrapper.set(PayTransferDO::getChannelErrorCode, transferVO.getChannelErrorCode());
            updateWrapper.set(PayTransferDO::getChannelErrorMsg, transferVO.getChannelErrorMsg());
            if (StrUtil.isNotBlank(transferVO.getChannelTransferNo())) {
                updateWrapper.set(PayTransferDO::getChannelTransferNo, transferVO.getChannelTransferNo());
            }
            if (StrUtil.isNotBlank(transferVO.getChannelPackageInfo())) {
                updateWrapper.set(PayTransferDO::getChannelPackageInfo, transferVO.getChannelPackageInfo());
            }
            if (UnifiedTransferStatusEnum.isSuccess(transferVO.getStatus())) {
                updateWrapper.set(PayTransferDO::getStatus, UnifiedTransferStatusEnum.SUCCESS.getValue());
                updateWrapper.set(PayTransferDO::getSuccessTime, LocalDateTime.now());
            } else if (UnifiedTransferStatusEnum.isClosed(transferVO.getStatus())) {
                updateWrapper.set(PayTransferDO::getStatus, UnifiedTransferStatusEnum.CLOSED.getValue());
            } else if (UnifiedTransferStatusEnum.isDealing(transferVO.getStatus())) {
                updateWrapper.set(PayTransferDO::getStatus, UnifiedTransferStatusEnum.DEALING.getValue());
            }
            boolean b = payTransferMapper.update(updateWrapper) > 0;
            log.info("[syncTransfer][outTransferNo({}) 的 rawData({}) status({}) 更新结果 {}]", transfer.getTransferNo(), rawData, transferVO.getStatus(), b);
            if (b) {
                // 此处需要读取最新的状态
                transfer = payTransferMapper.selectByNo(transfer.getNo());
                if (UnifiedTransferStatusEnum.isSuccess(transfer.getStatus())) {
                    // 发送转账成功消息
                    transferProducer.sendTransferSuccessMessage(transfer.getNo(), transfer.getTransferNo());
                } else if (UnifiedTransferStatusEnum.isClosed(transfer.getStatus())) {
                    // 发送转账失败消息
                    transferProducer.sendTransferFailureMessage(transfer.getNo(), transfer.getTransferNo());
                } else if (UnifiedTransferStatusEnum.isDealing(transfer.getStatus())) {
                    // 发送转账进行中消息
                    transferProducer.sendTransferDealingMessage(transfer.getNo(), transfer.getTransferNo());
                }
            }
            return b;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public String notify(String channelCode, Map<String, String> params, String body, Map<String, String> headers) {
        log.info("[transfer/notify][channelCode({}) 收到回调数据] params: {}\nbody: {}\nheaders: {}", channelCode, params, body, headers);
        UnifiedTransferVO transferVO;
        if (Objects.equals(UnifiedTransferChannelEnum.MALLBOOK.getCode(), channelCode)) {
            transferVO = SpringUtil.getBean(WxLitePayClient.class).parseTransferNotify(params, body, headers);
        } else if (Objects.equals(UnifiedTransferChannelEnum.WX_BALANCE.getCode(), channelCode)) {
            transferVO = SpringUtil.getBean(MallBookWxLitePayClient.class).parseTransferNotify(params, body, headers);
        } else {
            throw new KnownServiceException(GlobalErrorCodeConstants.NOT_IMPLEMENTED.getCode(), "渠道回调未实现：" + channelCode);
        }
        PayTransferDO transferDO = payTransferMapper.selectByTransferNo(transferVO.getTransferNo());
        Assertions.notNull(transferDO, "转账单不存在：" + transferVO.getTransferNo());
        syncTransfer(transferDO);
        return JsonUtils.toJsonString(transferVO);
    }

}
